Skip to content

Fix #14105: Inconsistent type inference for anonymous functions#4913

Closed
phpstan-bot wants to merge 1 commit into2.1.xfrom
create-pull-request/patch-aq8uosv
Closed

Fix #14105: Inconsistent type inference for anonymous functions#4913
phpstan-bot wants to merge 1 commit into2.1.xfrom
create-pull-request/patch-aq8uosv

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

PHPStan inferred different return types for identical anonymous functions depending on where they were defined. A closure assigned to a class constant lost its precise return type (e.g., Closure(int): array instead of Closure(int): array{num: int}), while the same closure assigned to a top-level constant was inferred correctly.

Changes

  • Added closure body return type inference to src/Reflection/InitializerExprTypeResolver.php:
    • inferClosureReturnType() - collects return expressions from closure body and resolves their types
    • resolveExprTypeWithVariables() - resolves expressions with awareness of closure parameter types (variables normally fall through to MixedType in InitializerExprTypeResolver)
    • collectReturnExpressions() - recursively walks statement nodes to find return statements, skipping nested closures/functions/classes
    • intersectButNotNever() - intersects declared return type annotation with inferred return type (ported from MutatingScope)
  • New regression test in tests/PHPStan/Analyser/nsrt/bug-14105.php
  • Updated CLAUDE.md with documentation about this pattern

Root cause

InitializerExprTypeResolver is used to resolve types for constant expressions (class constant initializers, etc.) without a full analysis scope. For static closures, it only read the return type annotation (e.g., array) without analyzing the closure body. In contrast, MutatingScope::getClosureType() performs full body analysis during the main analysis phase, which is why top-level constants (resolved through the normal analysis pipeline) got the correct precise type.

The fix adds lightweight closure body analysis to InitializerExprTypeResolver: it collects return expressions, resolves their types using parameter type information, and intersects the result with the declared return type annotation to produce a more precise type.

Test

The regression test in tests/PHPStan/Analyser/nsrt/bug-14105.php verifies that both class-level and top-level constants with closure values are inferred as Closure(int): array{num: int}.

Fixes phpstan/phpstan#14105

- InitializerExprTypeResolver now infers closure return types from the body
- Builds parameter variable type map and passes it through expression resolution
- Uses intersectButNotNever() to combine inferred return type with annotation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14105.php

Closes phpstan/phpstan#14105
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent type inference for anonymous functions

2 participants